package org.bioversityinternational.mgbms.ws;

import java.io.NotSerializableException;
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;

import javax.jws.WebService;

import org.apache.cxf.interceptor.security.AccessDeniedException;
import org.apache.cxf.interceptor.security.AuthenticationException;

/**
 * @author SEMPERE
 *
 */
@WebService
public interface MgbmsService
{
	public static final String ACCEPTANCE_METHOD_SIGNATURE = "signature";
	public static final String ACCEPTANCE_METHOD_SHRINKWRAP = "shrinkwrap";
	public static final String ACCEPTANCE_METHOD_CLICKWRAP = "clickwrap";

	public static final String[] acceptanceMethods = {ACCEPTANCE_METHOD_SIGNATURE, ACCEPTANCE_METHOD_SHRINKWRAP, ACCEPTANCE_METHOD_CLICKWRAP};

	public static final String PAYMENT_METHOD_6_7 = "6.7";
	public static final String PAYMENT_METHOD_6_11 = "6.11";
	
	public static final String[] paymentMethods = {PAYMENT_METHOD_6_7, PAYMENT_METHOD_6_11};

	/**
	 * Lists all centres considered valid in the database. 
	 * (At this stage, a centre is considered valid if it has a name and an acronym)
	 * 
	 * @return	an id-to-acronym HashMap
	 */
	public HashMap<Integer, String> getListOfAvailableCentres();

	/**
	 * Lists distinct values that are currently used contact titles in the database. 
	 * 
	 * @return	an id-to-acronym HashMap
	 */
	public List<String> getDistinctContactTitleList();
	
	/**
	 * Generic method that can retrieve specified fields for a given set of objects 
	 * (At this stage, a centre is considered valid if it has a name and an acronym)
	 * 
	 * @param objectClassSimpleName		A domain class simple name, i.e. Centre , Accession, Contact
	 * @param idList					The list of IDs (as strings) that identify the objects of the specified type
	 * @param fieldNames				The list of fields to retrieve for each object (you can navigate through FK-associated objects using dot ".")
	 * @return							An id-to-dto HashMap
	 * @throws AccessDeniedException
	 * 
	 * N.B. 1:	The specified list of fields must contain only "simple-type" field names (String, Integer, Boolean or Timestamp). This method does not support returning an FK-associated object as a whole (you cannot ask for field "country" when working on Contact objects). However it is able to retrieve any field of the associated objects as long as they are explicitly specified ("country.label" works).
	 * N.B. 2:	If you use "." to navigate through FK-associated objects, be aware that an optional FK which is not associated will lead into a null result for the entire record (this limitation may be gotten over using HQL left joins but this is not a priority for now)
	 * N.B. 3:	Since it can return virtually any field from any table-record, this method is a potential security risk. This issue has been addressed in the MgbmsDB project (BaseDao.properties file) by making it possible to declare which fields of each domain-class are not allowed to be returned (example: forbiddenPropertiesForDomainClass.Login = password). You can specify several field names, separated by commas.
	 * 
	 * Usage example:	HashMap<String, DTO> centreDTOs = mgbmsService.getObjectFieldsByIDs("Centre", Arrays.asList(new String[] {"700", "503", "774"}), Arrays.asList(new String[] {"id", "pidCode", "name", "address.zipCode", "address.city", "address.country.label"}));
	 * 
	 */
	public HashMap<String, DTO> getObjectFieldsByIDs(String objectClassSimpleName, List<String> idList, List<String> fieldNames) throws AccessDeniedException;
	
	/**
	 * Gets all fields of a Contact along with its details (address and country), found by its e-mail address
	 * There is currently no unicity constraint on the email field at the database level, so this method will throw an Exception if the number of records with the given e-mail address is > 1 or return null if no match is found
	 * 
	 * @param email		The e-mail address of the contact being searched for
	 * @return			A DTO representing a Contact object
	 */
	public DTO getContactByEmail(String email) throws AccessDeniedException;

	
	/**
	 * Gets the centre which has the most recent association start_date among the ones the given contact is linked to
	 * 
	 * @param contactId		Contact Identifier
	 * @return				A DTO representing a Centre object, its address and address-country
	 */
	public DTO getCurrentCentreForContact(Integer contactId);
	
	/**
	 * Lists all countries found in the database. 
	 * 
	 * @return	an id-to-acronym label HashMap
	 */
	public HashMap<Integer, String> getListOfAvailableCountries();
	
	/**
	 * Lists all centre-types considered valid in the database. 
	 * (At this stage, a centre-type is considered valid if it has a code and an label)
	 * 
	 * @return	an id-to-label HashMap
	 */
	public HashMap<Integer, String> getListOfAvailableCentreTypes();
	
	/**
	 * Gets an address using its identifier
	 * 
	 * @param id			Address Identifier
	 * @return				A DTO representing an Address object and its country
	 */
	public DTO getAddressById(Integer id);
	
	/**
	 * Gets a centre using its identifier
	 * 
	 * @param id			Centre Identifier
	 * @return				A DTO representing a Centre object, its address and address-country
	 */
	public DTO getCentreById(Integer id);
	
	/**
	 * Creates a user account or updates an existing one. If no login record exists for this contact then one will be created (with password generation)
	 * 
	 * @param contactDetails
	 * @param addressDetails
	 * @param centreId
	 * @param newCentreDetails
	 * @param newCentreAddressDetails
	 * @return the login id
	 * @throws AccessDeniedException
	 * 
	 * - contactDetails and addressDetails must be provided and filled-in
	 * - an id field must be provided for contactDetails if you wish to update an existing contact record, otherwise a new one will be created
	 * - in case of an update, any unspecified or null field will be ignored (values in the DB will be kept), so you cannot set a field to null using the WS, however you can set empty strings in order to clear out DB fields 
	 * - a null centreId will be interpreted as "this contact works for a centre which is not in our original database"
	 * - if centreId is null then newCentreDetails and newCentreAddressDetails must be fully filled-in
	 * - you can provide a centreId AND a non-empty centreDetails object in order to correct the details of an existing centre (by adding a new_centre record)
	 * - if no new_centre record needs to be created but the centre address must be corrected, just pass a null newCentreDetails and a filled-in newCentreAddressDetails
	 * - any "id" field values in addressDetails, newCentreDetails or newCentreAddressDetails will be ignored (relationships are managed internally)
	 * - lastUpdated fields do not need to be passed in, their value will be set automatically
	 */
	public Integer saveOrUpdateUserAccount(DTO contactDetails, DTO addressDetails, Integer centreId, DTO newCentreDetails, DTO newCentreAddressDetails) throws AccessDeniedException;
	
	/**
	 * Generates a random 8-byte password for a user (Login domain object)
	 * 
	 * @param userName		The e-mail address of the user's associated Contact object
	 * @param pwd			The user's current password
	 * @throws AuthenticationException, AccessDeniedException
	 * 
	 * The generated password is then sent by e-mail to the given address
	 */
	public void generateNewUserPassword(String userName, String pwd) throws AuthenticationException, AccessDeniedException;
	
	/**
	 * Gets a user (Login domain object) along with its details (address, country and newCentre), from its e-mail address and password
	 * 
	 * @param userName		The e-mail address of the user's associated Contact object
	 * @param pwd			The user's current password
	 * @return 				A DTO representing a Login object, the corresponding Contact", Contact Address, NewCentre, NewCentre Address, NewCentre Address Country, NewCentre CentreType, NewCentre Country, NewCentre parent (partOf field)
	 * @throws AccessDeniedException
	 * 
	 * The generated password is then sent by e-mail to the given address
	 */
	public DTO getUserDetails(String userName, String pwd) throws AuthenticationException;
	
	public String generateRegistrationCode(String sUserEmail) throws AccessDeniedException;

	public HashMap<Integer, String> getAvailableMaterialTypesForAccession(String itcCode) throws AccessDeniedException;
	
	public Integer createOrderRequest(Integer contactId, String sAuthorisedOfficialName, String sAuthorisedOfficialAddress, String acceptanceMethod, String paymentMethod, HashMap<String, Integer> accessionsAlongWithMaterialTypes, String purpose) throws AccessDeniedException;
	
	/**
	 * Gets URL(s) of the SMTA document (and addendum if applicable) corresponding to the passed Order id
	 * 
	 * @param orderId		Order identifier
	 * @return 				A list of Strings representing URLs (size 1 or 2)
	 * @throws AccessDeniedException
	 * 
	 */
	public List<String> getSMTA(Integer orderId) throws AccessDeniedException;
	public LinkedHashMap<String, String> getSmtaAcceptanceMethods() throws AccessDeniedException;
	public LinkedHashMap<String, String> getSmtaPaymentMethods() throws AccessDeniedException;
	public String getSampleSMTA() throws AccessDeniedException;
	public List<DTO> getOrdersByContact(Integer contactId);
	public DTO getOrderById(Integer orderId);
	public List<DTO> getOrderDetails(Integer orderId);
	public List<DTO> getShipmentsByOrder(Integer orderId);
	public List<DTO> getShipmentDetails(Integer shipmentId);
	public List<String> getPredefinedPurposeList();
	public Boolean getAnnex1StatusForAccession(String itcCode) throws AccessDeniedException; 
	
	public class DTO
	{
		public HashMap<String, String> stringValues = new HashMap<String, String>();
		public HashMap<String, Integer> intValues = new HashMap<String, Integer>();
		public HashMap<String, Boolean> boolValues = new HashMap<String, Boolean>();
		public HashMap<String, Date> timestampValues = new HashMap<String, Date>();
		
		public DTO()
		{
		}
		
		public static Class[] getSupportedFieldTypes()
		{
			return new Class[] {String.class, Integer.class, Boolean.class, Timestamp.class};
		}
		
		public String[] getFieldNames(Class type) throws NotSerializableException
		{
			if (String.class.equals(type))
				return stringValues.keySet().toArray(new String[stringValues.size()]);
			else if (Integer.class.equals(type))
				return intValues.keySet().toArray(new String[intValues.size()]);
			else if (Boolean.class.equals(type))
				return boolValues.keySet().toArray(new String[boolValues.size()]);
			else if (Date.class.isAssignableFrom(type))
				return timestampValues.keySet().toArray(new String[timestampValues.size()]);
			else
				throw new NotSerializableException("Unsupported class " + type.getName() + " for DTO field types");
		}

		public void set(String name, Serializable val) throws NotSerializableException {
			if (val == null)
			{
				stringValues.remove(name);
				intValues.remove(name);
				boolValues.remove(name);
				timestampValues.remove(name);
			}
			else if (val instanceof String)
			{
				stringValues.put(name, (String)val);
				intValues.remove(name);
				boolValues.remove(name);
				timestampValues.remove(name);
			}
			else if (val instanceof Integer)
			{
				stringValues.remove(name);
				intValues.put(name, (Integer)val);
				boolValues.remove(name);
				timestampValues.remove(name);
			}
			else if (val instanceof Boolean)
			{
				stringValues.remove(name);
				intValues.remove(name);
				boolValues.put(name, (Boolean)val);
				timestampValues.remove(name);
			}
			else if (Date.class.isAssignableFrom(val.getClass()))
			{
				stringValues.remove(name);
				intValues.remove(name);
				boolValues.remove(name);
				timestampValues.put(name, (Date)val);
			}
			else
				throw new NotSerializableException("Don't know how to serialize instances of class " + val.getClass().getName() + " for field " + name);
		}

		public String getString(String fieldName) {
			return stringValues.get(fieldName);
		}
		
		public Integer getInt(String fieldName) {
			return intValues.get(fieldName);
		}
		
		public Boolean getBool(String fieldName) {
			return boolValues.get(fieldName);
		}
		
		public Timestamp getTimestamp(String fieldName) {
			return new Timestamp(timestampValues.get(fieldName).getTime());
		}
		
		public Serializable get(String fieldName) {
			Serializable result = stringValues.get(fieldName);
			if (result != null)
				return result;
			
			result = intValues.get(fieldName);
			if (result != null)
				return result;

			result = boolValues.get(fieldName);
			if (result != null)
				return result;
			
			result = timestampValues.get(fieldName);
			if (result != null)
				return result;
			
			return null;
		}

		public HashMap<String, String> getStringValues() {
			return stringValues;
		}

		public void setStringValues(HashMap<String, String> stringValues) {
			this.stringValues = stringValues;
		}

		public HashMap<String, Integer> getIntValues() {
			return intValues;
		}

		public void setIntValues(HashMap<String, Integer> intValues) {
			this.intValues = intValues;
		}

		public HashMap<String, Boolean> getBoolValues() {
			return boolValues;
		}

		public void setBoolValues(HashMap<String, Boolean> boolValues) {
			this.boolValues = boolValues;
		}

		public HashMap<String, Date> getTimestampValues() {
			return timestampValues;
		}

		public void setTimestampValues(HashMap<String, Timestamp> timestampValues) {
			this.timestampValues = new HashMap<String, Date>();
			for (String key : timestampValues.keySet())
				this.timestampValues.put(key, timestampValues.get(key));
		}
	}
	
}
